•  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  


State Machine 패턴

목차
1. 개요2. 동기3. 구성 요소
3.1. 상태 정의 - enum3.2. 상태 검증 - modifier3.3. 전이 함수3.4. 시간 기반 전이 - timedTransition3.5. 이벤트
4. 구현 패턴
4.1. 선형 전이4.2. 비선형 전이 - 전이 테이블4.3. 비트마스크 최적화
5. 보안 고려사항
5.1. Reentrancy (재진입 공격)5.2. block.timestamp 조작5.3. Front-running
6. 활용 사례7. 관련 패턴8. 관련 문서

1. 개요 [편집]

State Machine 패턴은 컨트랙트의 생애 주기를 유한한 상태(state)들로 나누고, 각 상태에서 허용하는 행위와 전이(transition) 규칙을 명시적으로 정의하는 패턴이다.[1]

컴퓨터 과학의 유한 상태 기계(FSM) 이론에 기반한다.[2]

컨트랙트는 반드시 하나의 상태에만 존재하며 사전에 정의된 규칙에 따라서만 상태가 변경된다.

2. 동기 [편집]

스마트 컨트랙트는 배포 후 코드 수정이 불가능하다. 그런데 경매, 에스크로, 거버넌스 등 대부분 온체인 비즈니스 로직은 여러 단계를 순서대로 거치는 생애주기를 가진다.

경매에서 정산이 끝난 뒤 입찰을 넣거나 에스크로에서 입금 전에 물건 수령을 확인하는 것은 허용되면 안 된다.

이러한 "잘못된 시점에 잘못된 함수가 호출되는" 버그는 전통 소프트웨어에서는 패치로 해결할 수 있지만 불변(immutable)한 스마트 컨트랙트에서는 자금 손실로 직결된다.[3]

State Machine 패턴은 이 문제를 해결하기 위해 설계되었다.

이 패턴이 해결하는 핵심 문제는 다음과 같다.
  • 단계 간 순서 강제 - 정의되지 않은 경로로의 상태 전이를 구조적으로 차단한다.
  • 함수 호출 시점 제어 - 각 함수가 실행 가능한 상태를 컴파일 타임에 선언하여, 잘못된 시점의 호출을 런타임에서 revert한다.
  • 시간 조건 자동 적용 - 블록체인에 cron이 없는 환경에서 시간 경과에 따른 단계 전환을 트랜잭션 호출 시점에 자동 반영한다.
  • 감사 용이성 - 상태와 전이를 다이어그램으로 시각화할 수 있어 코드 리뷰와 보안 감사가 용이해진다.[1]

3. 구성 요소 [편집]

3.1. 상태 정의 - enum [편집]


enum Stage {
Created,
Bidding,
Revealing,
Settled
}
Stage public currentStage = Stage.Created;


내부적으로 uint8로 저장된다. 정의되지 않은 값으로의 캐스팅은 revert된다. (Solidity 0.8+)[4]

3.2. 상태 검증 - modifier [편집]


modifier atStage(Stage expected) {
require(currentStage == expected, "Invalid stage");
_;
}


함수에 atStage(Stage.Bidding)을 붙이면 Bidding 상태에서만 실행된다.

3.3. 전이 함수 [편집]

선형 전이 - 순서대로만 진행:

function nextStage() internal {
currentStage = Stage(uint(currentStage) + 1);
}


명시적 전이 - 허용된 경로만:

function transitionTo(Stage next) internal {
require(allowedTransitions[currentStage][next]);
currentStage = next;
}

3.4. 시간 기반 전이 - timedTransition [편집]

블록체인에는 cron이 없으므로 다음 트랜잭션 호출 시 시간 경과를 검사한다.[5]


modifier timedTransition() {
if (currentStage == Stage.Bidding
&& block.timestamp >= createdAt + BIDDING_DURATION)
currentStage = Stage.Revealing;
if (currentStage == Stage.Revealing
&& block.timestamp >= createdAt + BIDDING_DURATION + REVEAL_DURATION)
currentStage = Stage.Settled;
_;
}


주의: if를 연속 사용한다(else if 아님). 오랜 기간 트랜잭션이 없었을 때 한 번의 호출로 연쇄 전이가 가능해야 하기 때문이다.

3.5. 이벤트 [편집]


event StageChanged(Stage indexed from, Stage indexed to, address indexed triggeredBy);


모든 상태 전이에서 이벤트를 발행하여 오프체인 추적을 가능하게 한다.

4. 구현 패턴 [편집]

4.1. 선형 전이 [편집]

상태가 순차적으로만 진행되는 경우. nextStage()uint(currentStage) + 1 계산.

경매, ICO, 투표, 타임락에 적합하다.

4.2. 비선형 전이 - 전이 테이블 [편집]

분기·복귀가 있는 워크플로우에서 사용한다. 허용된 전이를 mapping으로 정의한다.[1]


mapping(Stage => mapping(Stage => bool)) private allowedTransitions;

constructor() {
allowedTransitions[Stage.AwaitingPayment][Stage.Funded] = true;
allowedTransitions[Stage.Funded][Stage.Delivered] = true;
allowedTransitions[Stage.Funded][Stage.Disputed] = true;
allowedTransitions[Stage.Disputed][Stage.Delivered] = true; // resolve
allowedTransitions[Stage.Disputed][Stage.Cancelled] = true; // cancel
}

4.3. 비트마스크 최적화 [편집]

이중 mapping은 검증당 SLOAD 2회가 필요하다. 비트마스크를 사용하면 SLOAD 1회 + 비트 연산으로 줄일 수 있다.[6]


mapping(Stage => uint8) private transitionMap;

// AwaitingPayment -> Funded(bit 1) 허용
transitionMap[Stage.AwaitingPayment] = 1 << uint8(Stage.Funded);

function transitionTo(Stage next) internal {
require(transitionMap[currentStage] & (1 << uint8(next)) != 0);
currentStage = next;
}

방식
가스 비용 (검증 1회)
적합한 경우
이중 mapping
~4,200 gas (SLOAD × 2)
가독성 우선
비트마스크
~2,100 gas (SLOAD × 1)
호출 빈번, 가스 최적화 필요
선형 nextStage()
~200 gas (연산만)
순차 전이만 있을 때

5. 보안 고려사항 [편집]

5.1. Reentrancy (재진입 공격) [편집]

상태 전이 함수가 외부 컨트랙트에 ETH를 송금하거나 외부 함수를 호출할 때, 호출받은 컨트랙트가 원래 함수를 다시 호출(재진입)할 수 있다. 이때 상태가 아직 변경되지 않았다면, atStage modifier를 통과하여 동일한 함수가 반복 실행된다.[3]

대응은 Checks-Effects-Interactions(CEI) 패턴이다. 상태 변경(Effect)을 외부 호출(Interaction)보다 먼저 수행하면, 재진입 시 이미 변경된 상태에 의해 atStage에서 revert된다.[7]

5.2. block.timestamp 조작 [편집]

시간 기반 전이(timedTransition)는 block.timestamp에 의존한다. 블록 제안자는 이 값을 실제 시간 대비 약 15초 범위 내에서 조작할 수 있다.[8] 따라서 각 단계의 최소 길이를 수 분 이상으로 설정해야 한다.

5.3. Front-running [편집]

상태 전이를 발생시키는 트랜잭션이 mempool에 공개되면, 공격자가 이를 관찰하고 더 높은 가스비로 자신의 트랜잭션을 먼저 포함시킬 수 있다.[9] 대응으로는 commit-reveal 스킴이나 전이 전 최소 지연 시간을 두는 방법이 있다.

6. 활용 사례 [편집]

분야
예시
상태 흐름
DeFi 거버넌스
Aave, Compound Governor
Pending → Active → Succeeded/Defeated → Queued → Executed
NFT 민팅
ERC-721 드롭
Inactive → Whitelist → Public → SoldOut → Revealed
에스크로
결제 보호
AwaitingPayment → Funded → Delivered → Complete
공급망
제품 추적
Manufactured → Shipped → InTransit → Delivered → Verified
스테이킹
PoS 프로토콜
Unlocked → Staked → Cooldown → Withdrawable

7. 관련 패턴 [편집]


8. 관련 문서 [편집]

[1] 1.1 1.2 1.3 Wohrer, Maximilian; Zdun, Uwe (2018). Smart Contracts: Security Patterns in the Ethereum Ecosystem and Solidity. 2018 International Workshop on Blockchain Oriented Software Engineering (IWBOSE). IEEE. pp. 2–8.[2] Hopcroft, John E.; Ullman, Jeffrey D. (1979). Introduction to Automata Theory, Languages, and Computation. Addison-Wesley. ISBN 0-201-02988-X.[3] 3.1 3.2 Atzei, Nicola; Bartoletti, Massimo; Cimoli, Tiziana (2017). A Survey of Attacks on Ethereum Smart Contracts (SoK). Principles of Security and Trust. Springer. pp. 164–186.[4] Solidity Documentation. Enums. docs.soliditylang.org.[5] Solidity Documentation. Common Patterns: State Machine. docs.soliditylang.org.[6] Ethereum Yellow Paper. Ethereum: A Secure Decentralised Generalised Transaction Ledger. Appendix G. Fee Schedule.[7] ConsenSys Diligence. Reentrancy. Smart Contract Best Practices.[8] ConsenSys Diligence. Timestamp Dependence. Smart Contract Best Practices.[9] Daian, Philip et al. (2020). Flash Boys 2.0. 2020 IEEE Symposium on Security and Privacy. pp. 910–927.